﻿#region License
/*  YanYeovil
    Copyright (c) 2014, Alexey A. Popov
    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification, are
    permitted provided that the following conditions are met:

    - Redistributions of source code must retain the above copyright notice, this list
      of conditions and the following disclaimer.

    - Redistributions in binary form must reproduce the above copyright notice, this list
      of conditions and the following disclaimer in the documentation and/or other materials
      provided with the distribution.

    - Neither the propertyName of the Alexey A. Popov nor the names of its contributors may be used to
      endorse or promote products derived from this software without specific prior written
      permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *AS IS* AND ANY EXPRESS
    OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
    IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;

namespace Pvax.Data
{
    /// <summary>
    /// A base class for objects that can be stored in a database. Provides change notificatiosn and
    /// implements some useful interfaces.
    /// </summary>
    /// <typeparam name="T">The type of the primary key.</typeparam>
    /// <seealso cref="IId{T}"/>
    [Serializable]
    public class DbStoreable<T>: IId<T>, IDbStoreable, INotifyPropertyChanged, INotifyPropertyChanging
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DbStoreable{T}"/> to an
        /// empty object, without setting it's <c>Id</c> property.
        /// </summary>
        public DbStoreable()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DbStoreable{T}"/> and sets 
        /// it's <c>Id</c> property to <paramref name="id"/>.
        /// </summary>
        /// <param name="id">The initial ID of the object.</param>
        public DbStoreable(T id)
        {
            this.id = id;
        }

        #region IId<TKey> Members

        T id;

        /// <summary>
        /// Gets or sets the primary key for the object.
        /// </summary>
        /// <value>
        /// The value of the primary key.
        /// </value>
        public T Id
        {
            get
            {
                return id;
            }
            set
            {
                if(!EqualityComparer<T>.Default.Equals(id, value))
                {
                    OnPropertyChanging("Id");
                    id = value;
                    OnPropertyChanged("Id");
                }
            }
        }

        #endregion

        #region IDbStoreable Members

        [Flags]
        enum State
        {
            New = 1,

            Dirty = 2
        }

        [NonSerialized]
        State state = State.New;

        void SetIsDirty(bool value)
        {
            bool dirty = 0 != (state & State.Dirty);
            if(dirty != value)
            {
                if(value)
                    state |= State.Dirty;
                else
                    state &= ~State.Dirty;
            }
        }

        /// <summary>
        /// Gets <c>true</c> is the object has not yet been pushed to the database;
        /// <c>false</c> otherwise.
        /// </summary>
        bool IDbStoreable.IsNew
        {
            get
            {
                return 0 != (state & State.New);
            }
        }

        /// <summary>
        /// Sets the new flag to <c>false</c>.
        /// </summary>
        void IDbStoreable.Touch()
        {
            state &= ~State.New;
        }

        /// <summary>
        /// Gets the dirty flag value, i. e. if it is <c>true</c> the object has been changed.
        /// </summary>
        bool IDbStoreable.IsDirty
        {
            [DebuggerStepThrough]
            get
            {
                return 0 != (state & State.Dirty);
            }
        }

        /// <summary>
        /// Resets the dirty flag to <c>false</c>.
        /// </summary>
        void IDbStoreable.Clear()
        {
            SetIsDirty(false);
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Use in derived classes to check at run time if the object supports
        /// the property with specified name.
        /// </summary>
        /// <param name="name">The name of the property to check.</param>
        /// <remarks>
        /// Used for debugging purposes.
        /// </remarks>
        [Conditional("DEBUG")]
        void VerifyPropertyName(string name)
        {
            if(String.IsNullOrEmpty(name))
                return;
            Type type = GetType();
            PropertyInfo propInfo = type.GetProperty(name);
            if(null == propInfo)
            {
                string message = String.Format(Properties.Resources.ErrNoProperty, type.Name, name);
                Debug.Fail(message, new StackTrace().ToString());
            }
        }

        /// <summary>
        /// Gets filred when a property is cnahed.
        /// </summary>
        [field:NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Fires a <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="e">An instance of <see cref="PropertyChangedEventArgs"/> that describes
        /// the property that's changed.</param>
        [DebuggerStepThrough]
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if(null == e)
                e = new PropertyChangedEventArgs(String.Empty);
            VerifyPropertyName(e.PropertyName);
            PropertyChangedEventHandler handler = PropertyChanged;
            if(null != handler)
                handler(this, e);
            SetIsDirty(true);
        }

        /// <summary>
        /// Fires a <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the property which value has been changed.</param>
        [DebuggerStepThrough]
        protected void OnPropertyChanged(string propertyName)
        {
            if(null == propertyName)
                propertyName = String.Empty;
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region INotifyPropertyChanging Members

        /// <summary>
        /// Gets fired right before a property changes.
        /// </summary>
        [field: NonSerialized]
        public event PropertyChangingEventHandler PropertyChanging;

        /// <summary>
        /// Fires <see cref="PropertyChanging"/> event.
        /// </summary>
        /// <param name="e">An instance of a <see cref="PropertyChangingEventArgs"/> describing
        /// the property that has been changed.</param>
        [DebuggerStepThrough]
        protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
        {
            if(null == e)
                e = new PropertyChangingEventArgs(String.Empty);
            VerifyPropertyName(e.PropertyName);
            PropertyChangingEventHandler handler = PropertyChanging;
            if(null != handler)
                handler(this, e);
        }

        /// <summary>
        /// Fires <see cref="PropertyChanging"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the property that is about to change.</param>
        [DebuggerStepThrough]
        protected void OnPropertyChanging(string propertyName)
        {
            if(null == propertyName)
                propertyName = String.Empty;
            OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
        }

        #endregion
    }
}
